#define DEFAULT_TO_CHECKING_REPLICATION

using System;
using System.Collections.Generic;
using System.DirectoryServices.Protocols;
using System.Text;
using System.Xml;
using System.Xml.Serialization;
using ActiveDirectoryUtilities.CommonLibrary;
using ActiveDirectoryUtilities.DirectoryServiceAccess;
using System.Data;

namespace ActiveDirectoryUtilities.ReplDiag
{
	partial class Program
	{
		const string CMD_LINE_SWITCH_SAVE_RAW_DATA = "Save";
		const string CMD_LINE_SWITCH_IMPORT_RAW_DATA = "ImportData";
		const string CMD_LINE_SWITCH_CHECK_REPLICATION = "CheckForStableReplTopology";
		const string CMD_LINE_SWITCH_CLEAN_LINGERING_OBJECTS = "RemoveLingeringObjects";
		const string CMD_LINE_SWITCH_RUN_SINGLE_THREADED = "SingleThreaded";
		const string CMD_LINE_SWITCH_NUM_THREADS = "NumThreads";
		const string CMD_LINE_SWITCH_TEST_CASES = "ShowTestCases";
		const string CMD_LINE_SWITCH_USE_ADVISORY_MODE = "AdvisoryMode";
		const string CMD_LINE_SWITCH_OUTPUT_LO_COMMAND_LINE = "OutputRepadminCommandLineSyntax";
		const string CMD_LINE_SWITCH_SPECIFY_REFERENCE_DC = "OverrideDefaultReferenceDC";
		const string CMD_LINE_SWITCH_USE_ROBUST_DC_LOCATION = "UseRobustDCLocation";

		//These will prevent the cleaning process from being thorough, thus they are not documented anywhere except for here and are case sensitive.
		const string CMD_LINE_SWITCH_BYPASS_REPLICATION_STABILITY_CHECK = "BypassStabilityCheck";
		const string CMD_LINE_SWITCH_BYPASS_CLEANING_WRITABLE_NC = "BypassCleaningWritableNC";
		const string DEFAULT_SAVE_FILE_NAME = "ReplicationData.xml";

		static private List<DomainControllerReplicationInfo> _dcs;
		static Dictionary<string, string> _referenceDCOverride = new Dictionary<string, string>();
		static List<string> _bypassCleaningWritableNCs = new List<string>();

		static bool _buildLoCleanTopology = false
					, _saveDataWhenDone = false
					, _useAdvisoryMode = false
					, _outputRemoveLingeringObjectsCommandLine = false
					, _bypassReplicationStabilityCheck = false
					, _useRobustDcLocation = false;
#if DEFAULT_TO_CHECKING_REPLICATION
		static bool _checkForStableReplTopology = true;	//Set to true here.
#else
		static bool _checkForStableReplTopology = false;
#endif

		static int _numThreads = 100;

		static void Main(string[] args)
		{
			//AppDomain.CurrentDomain.UnhandledException += new UnhandledExceptionEventHandler(CurrentDomain_UnhandledException);

			outputHeader();

			// if (!((Environment.Version.Major >= 2) && (Environment.Version.Build >= 3053)))
            // we need at least .net 2.0 SP1. Higher is OK. 
            if ((Environment.Version.Major < 2) || ((Environment.Version.Major == 2) && (Environment.Version.Revision < 1433)))
            {
                Console.WriteLine(".NET 2.0 SP1 does not appear to be installed (detected " +
                    Environment.Version.ToString() + ") .  Please goto http://www.microsoft.com/downloads and install .NET 2.0 SP1");
                return;
            }

			if (!parseCommandLineArgs(args))
			{
				return;
			}

			//Assumes that if _dcs was populated it happened due to a command line parameter that was passed.
			if (_dcs == null)
			{
				long start = DateTime.Now.Ticks;

				_dcs = new List<DomainControllerReplicationInfo>();

				WorkItemQueue wiq = new WorkItemQueue(_numThreads);
				Forest f = null;
				try
				{
					f = Forest.GetCurrentForest();
				}
				catch (LdapException ex)
				{
					if (ex.ErrorCode == 81)
					{
						Console.WriteLine("Couldn't contact a DC for the forest root.  Ensure DNS 'A' records for the forest root domain are properly registered.");
						return;
					}
				}

				Console.WriteLine("Enumerating Forest:  {0}", f.ForestFqdn);
				Console.WriteLine("\tForest Functional Level:  {0}", f.ForestFunctionalLevel.ToString().Replace('_', ' '));
				foreach (Domain d in f.Domains)
				{
					Console.Write("Enumerating Domain:  {0}", d.DnsDomainName);
					try
					{
						List<DomainController> dcs = d.FindAllDomainControllers(_useRobustDcLocation);
						Console.WriteLine("\t- Found {0} DCs.", dcs.Count);
						Console.WriteLine("\tDomain Functional Level:  {0}", d.MsDsBehaviorVersion.ToString().Replace('_', ' '));
						if (d.MsDsBehaviorVersion <= DomainMode.Windows_2003_Interim)
						{
							Console.WriteLine("\tIs Mixed Mode:  {0}", d.IsNtMixedDomain.ToString());
						}
						foreach (DomainController dc in dcs)
						{
							if ((dc.IsDsEnabled) && (!string.IsNullOrEmpty(dc.DnsHostName)))
							{
								DomainControllerReplicationInfo dcri = new DomainControllerReplicationInfo(dc.DnsHostName);
								dcri.NtDsaGuid = dc.NtdsDsaObjectGuid;
								dcri.SiteName = dc.SiteName.ToLower();
								dcri.NtdsDsaObjectName = dc.NtdsDsaObjectName;
								_dcs.Add(dcri);
								wiq.EnqueueWorkItem(ProcessReplicationInformation.CollectDCInformation, dcri);
							}
							else if ((dc.IsDsEnabled) && (string.IsNullOrEmpty(dc.DnsHostName)))
							{
								DomainControllerReplicationInfo dcri = new DomainControllerReplicationInfo(dc.NetBiosName);
								dcri.NtDsaGuid = dc.NtdsDsaObjectGuid;
								dcri.SiteName = dc.SiteName.ToLower();
								dcri.NtdsDsaObjectName = dc.NtdsDsaObjectName;
								dcri.ErrorMessage = string.Format("dnsHostName attribute not populated on {0}.  This may be due to replication convergence during a new DC promotion or a missing connection object to pull from said DC.",  dc.ServerObjectName);
								_dcs.Add(dcri);
							}
							else
							{
								Console.WriteLine("\tDirectory Services are not running on {0}", dc.Name);
							}
						}
					}
					catch (DirectoryServiceAccessException ex)
					{
						if (string.Compare(ex.Message.Trim(), "Error 1355 getting Domain Controller list.", true) == 0)
						{
							Console.WriteLine("\t- Domain Could Not Be Contacted.");
						}
					}
				}
				wiq.WaitAllWorkItems();
				Console.WriteLine();
				Console.WriteLine("Data collection duration:  {0} seconds", TimeSpan.FromTicks(DateTime.Now.Ticks - start).TotalSeconds.ToString("N0"));
				Console.WriteLine();
			}

			ProcessReplicationInformation pri = new ProcessReplicationInformation(_dcs);
			if (!pri.IsReplicationToplogyStable)
			{
				for (int i = 0; i < pri.ReplicationTopologyStateData.Columns.Count; i++)
				{
					Console.Write("\"{0}\"", pri.ReplicationTopologyStateData.Columns[i].ColumnName);
					if (i != pri.ReplicationTopologyStateData.Columns.Count - 1)
					{
						Console.Write(",");
					}
				}
				Console.WriteLine();

				foreach (DataRow dr in pri.ReplicationTopologyStateData.Rows)
				{
                    if ((!_buildLoCleanTopology) || (_buildLoCleanTopology && (bool)dr[ProcessReplicationInformation.COLUMN_NAME_IS_CRITICAL]))
                    {
                        for (int i = 0; i < pri.ReplicationTopologyStateData.Columns.Count; i++)
                        {
                            if (dr[i] != DBNull.Value)
                            {
                                Console.Write("\"{0}\"", dr[i].ToString());
                            }
                            if (i != pri.ReplicationTopologyStateData.Columns.Count - 1)
                            {
                                Console.Write(",");
                            }
                        }
                        Console.WriteLine();
                    }
				}
			}

			if (_buildLoCleanTopology)
			{
				cleanNCs(pri);
			}
			else if (_checkForStableReplTopology)
			{
				if (pri.IsReplicationToplogyStable)
				{
					//if ((_cmdLineSwitchShowOnlyCriticalErrors) && (!showOnlyCriticalErrors))
					Console.WriteLine("No topology errors found.");
				}

				//If the topology is not stable, default to saving the data.
				if (!_saveDataWhenDone && !pri.IsReplicationToplogyStable)
				{
					Console.WriteLine("Significant topology errors found.  Saving data to {0}.", DEFAULT_SAVE_FILE_NAME);
					_saveDataWhenDone = true;
				}
			}

			if (_saveDataWhenDone)
			{
				if (saveData(".\\" + DEFAULT_SAVE_FILE_NAME))
				{
					Console.WriteLine("Saved Data as \"{0}\"", DEFAULT_SAVE_FILE_NAME);
				}
			}
		}

		private static bool parseCommandLineArgs(string[] args)
		{
			if (args.Length > 0)
			{
				foreach (string arg in args)
				{
					if (arg.Contains("/"))
					{
						if ((arg.ToLower().Contains(CMD_LINE_SWITCH_IMPORT_RAW_DATA.ToLower())) && (arg.Contains(":")))
						{
							string[] argParts = { arg.Substring(0, arg.IndexOf(':')), arg.Substring(arg.IndexOf(':') + 1) };
							if (argParts.Length > 1)
							{
								XmlSerializer xs = new XmlSerializer(typeof(List<DomainControllerReplicationInfo>));
								XmlTextReader xtr = new XmlTextReader(argParts[1]);
								try
								{
									_dcs = (List<DomainControllerReplicationInfo>)xs.Deserialize(xtr);
								}
								catch (InvalidOperationException e)
								{
									Console.WriteLine("Error loading saved data: \n\t" + e.InnerException.Message);
									xtr.Close();
									return false;
								}
								xtr.Close();
							}
						}
						else if ((arg.ToLower().Contains(CMD_LINE_SWITCH_SPECIFY_REFERENCE_DC.ToLower())) && (arg.Contains(":")))
						{
							string[] splitArg = arg.ToLower().Split(':');
							//2nd position should be an NC, 3rd position a computer name.
							if (splitArg.Length == 3)
							{
								if (!_referenceDCOverride.ContainsKey(splitArg[1]))
								{
									_referenceDCOverride.Add(splitArg[1].Replace('\"', ' ').Trim(), splitArg[2]);
								}
								else
								{
									_referenceDCOverride[splitArg[1].Replace('\"', ' ').Trim()] = splitArg[2];
								}
							}
							else
							{
								Console.WriteLine("Incorrect number of arguments, this should be in the format of \n\r\t{0} /{1}:\"dc=forestroot,dc=com\":server.forestroot.com.", System.Reflection.Assembly.GetExecutingAssembly().GetName().Name, CMD_LINE_SWITCH_SPECIFY_REFERENCE_DC);
								return false;
							}
						}
						else if ((arg.ToLower().Contains(CMD_LINE_SWITCH_NUM_THREADS.ToLower())) && (arg.Contains(":")))
						{
							try
							{
								_numThreads = Convert.ToInt32(arg.Split(':')[1]);
							}
							catch (FormatException)
							{
								Console.WriteLine("Make sure a number immediately follows the colon for the number of threads.\n");
								return outputHelp(arg);
							}
						}
						else if (arg.ToLower().Contains(CMD_LINE_SWITCH_CHECK_REPLICATION.ToLower()))
						{
							_checkForStableReplTopology = true;
						}
						else if (arg.ToLower().Contains(CMD_LINE_SWITCH_CLEAN_LINGERING_OBJECTS.ToLower()))
						{
							_buildLoCleanTopology = true;
						}
#if !DEFAULT_TO_CHECKING_REPLICATION
						else if (arg.ToLower().Contains(CMD_LINE_SWITCH_CHECK_REPLICATION.ToLower()))
						{
						    _checkForStableReplTopology = true;
						}
#endif
						else if (arg.ToLower().Contains(CMD_LINE_SWITCH_SAVE_RAW_DATA.ToLower()))
						{
							_saveDataWhenDone = true;
						}
						else if (arg.ToLower().Contains(CMD_LINE_SWITCH_TEST_CASES.ToLower()))
						{
							outputExtendedHelp();
							return false;
						}
						else if (arg.ToLower().Contains(CMD_LINE_SWITCH_USE_ADVISORY_MODE.ToLower()))
						{
							_useAdvisoryMode = true;
						}
						else if (arg.ToLower().Contains(CMD_LINE_SWITCH_USE_ROBUST_DC_LOCATION.ToLower()))
						{
							_useRobustDcLocation = true;
						}
						else if (arg.ToLower().Contains(CMD_LINE_SWITCH_OUTPUT_LO_COMMAND_LINE.ToLower()))
						{
							_outputRemoveLingeringObjectsCommandLine = true;
						}
						//This is intentionally left case sensitive
						else if (arg.Contains(CMD_LINE_SWITCH_BYPASS_REPLICATION_STABILITY_CHECK))
						{
							_bypassReplicationStabilityCheck = true;
						}
						//This is intentionally left case sensitive
						else if ((arg.Contains(CMD_LINE_SWITCH_BYPASS_CLEANING_WRITABLE_NC)) && (arg.Contains(":")))
						{
							string[] splitArg = arg.ToLower().Split(':');
							//2nd position should be an NC.
							if (splitArg.Length == 2)
							{
								_bypassCleaningWritableNCs.Add(splitArg[1].Replace('\"', ' ').Trim());
							}
							else
							{
								Console.WriteLine("Incorrect number of arguments, this should be in the format of \n\r\t{0} /{1}:\"dc=forestroot,dc=com\".", System.Reflection.Assembly.GetExecutingAssembly().GetName().Name, CMD_LINE_SWITCH_SPECIFY_REFERENCE_DC);
								return false;
							}
						}
						else if (arg.Contains("?"))
						{
							return outputHelp(string.Empty);
						}
						else
						{
							//We didn't find the switched typed.  Error out and return the help menu
							return outputHelp(arg);
						}
					}
					else    //Not a valid switch
					{
						return outputHelp(arg);
					}
				}
			}
			else
			{
				_checkForStableReplTopology = true;
			}

			//Since we made it all the way through output all the commandline switches used
			foreach (string arg in args)
			{
				Console.WriteLine("Command Line Switch:  {0}", arg);
			}
			Console.WriteLine();
			return true;
		}

		private static bool outputHelp(string incorrectSyntax)
		{
			Console.WriteLine();
			if (!string.IsNullOrEmpty(incorrectSyntax))
			{
				Console.WriteLine("Incorrect Syntax:  {0}", incorrectSyntax);
				Console.WriteLine();
			}
			Console.WriteLine("Command Line Options:  {0} [/{1}] [/{2}] [/{3}] [/{4}:<FileName.XML>] [/{5}] [/{6}:\"dc=namingcontext,dc=com\":domainController.namingcontext.com]", System.Reflection.Assembly.GetExecutingAssembly().GetName().Name, CMD_LINE_SWITCH_SAVE_RAW_DATA, CMD_LINE_SWITCH_CHECK_REPLICATION, CMD_LINE_SWITCH_CLEAN_LINGERING_OBJECTS, CMD_LINE_SWITCH_IMPORT_RAW_DATA, CMD_LINE_SWITCH_TEST_CASES, CMD_LINE_SWITCH_SPECIFY_REFERENCE_DC);
			Console.WriteLine();
			Console.WriteLine("/{0} -Query each and every DC for a list of DCs in\n\r\tforest.  Ensures replication instability does not cause any to be\n\r\tmissed.", CMD_LINE_SWITCH_USE_ROBUST_DC_LOCATION);
			Console.WriteLine("/{0} -Save out the data from the current environment to XML.  File is named \n\t\"{1}\" and is located in the current directory.", CMD_LINE_SWITCH_SAVE_RAW_DATA, DEFAULT_SAVE_FILE_NAME);
#if !DEFAULT_TO_CHECKING_REPLICATION
			Console.WriteLine("/{0} -Analyze the data set for replication issues.", CMD_LINE_SWITCH_CHECK_REPLICATION);
#endif
			Console.WriteLine("/{0} -Import the XML that was saved during a prior execution of this\n\tutility.  Run one of the other options to do something with the data.", CMD_LINE_SWITCH_IMPORT_RAW_DATA);
			Console.WriteLine("/{0} -Show detail about test cases.", CMD_LINE_SWITCH_TEST_CASES);
			Console.WriteLine();
			Console.WriteLine("Lingering Object Cleanup:");
			Console.WriteLine("/{0} -Use the current forest topology to clean all the\n\r\tNCs in the forest.\n\tWILL NOT CLEAN WINDOWS 2000 SYSTEMS!!!", CMD_LINE_SWITCH_CLEAN_LINGERING_OBJECTS);
			Console.WriteLine("/{0} -Check for lingering objects only, do not clean.\n\r\tMust be used with /{1}.", CMD_LINE_SWITCH_USE_ADVISORY_MODE, CMD_LINE_SWITCH_CLEAN_LINGERING_OBJECTS);
			Console.WriteLine("/{0} -Specify reference DC for a naming context when\n\r\twhen removing lingering objects, can be used multiple times for\n\r\tdifferent NCs.\n\r\tOnly functional if using /{1}.", CMD_LINE_SWITCH_SPECIFY_REFERENCE_DC, CMD_LINE_SWITCH_CLEAN_LINGERING_OBJECTS);
			Console.WriteLine("/{0} -Output the command line syntax for repadmin.\n\r\tOnly active in conjunction with /{1}.", CMD_LINE_SWITCH_OUTPUT_LO_COMMAND_LINE, CMD_LINE_SWITCH_CLEAN_LINGERING_OBJECTS);
			Console.WriteLine();
			Console.WriteLine("Example syntax:");
			Console.WriteLine("{0} /{1}", System.Reflection.Assembly.GetExecutingAssembly().GetName().Name, CMD_LINE_SWITCH_SAVE_RAW_DATA);
			Console.WriteLine("\t- Collect the AD replication topology from the environment and save it.");
			Console.WriteLine("{0} /{1}:\"{2}\"", System.Reflection.Assembly.GetExecutingAssembly().GetName().Name, CMD_LINE_SWITCH_IMPORT_RAW_DATA, DEFAULT_SAVE_FILE_NAME);
			Console.WriteLine("\t- Load in previously collected data and check replication status.");
			Console.WriteLine("{0} /{1} /{2}:\"cn=Configuration,dc=forestroot,dc=com\":dc1.forestroot.com /{2}:\"dc=forestroot,dc=com\":dc2.forestroot.com", System.Reflection.Assembly.GetExecutingAssembly().GetName().Name, CMD_LINE_SWITCH_CLEAN_LINGERING_OBJECTS, CMD_LINE_SWITCH_SPECIFY_REFERENCE_DC);
			Console.WriteLine("\t- Clean lingering objects in the forest.  Since cleans happen on a per\n\r\tNC basis, reference DCs must be overridden on a per NC basis.  Each\n\r\toverride must be specified separately on the command line.\n\r\tMust use the DNS FQDN for the DC.");
			return false;
		}

		private static void outputExtendedHelp()
		{
			Console.WriteLine();
			Console.WriteLine("When checking replication health this checks the following tests cases.");
			Console.WriteLine("\t-Link is currently in a failure state.");
			Console.WriteLine("\t-Link has never successfully completed a replication cycle.");
			Console.WriteLine("\t-Partition hosted on the server has no links that have successfully completed a replication cycle.");
			Console.WriteLine("\t-There is only one writable instance of the partition in the forest.");
			Console.WriteLine("\t-Partition has no inbound replication links.");
			Console.WriteLine("\t-Writable instance of the partition has no outbound replication links.");
			Console.WriteLine("\t-Partition has no inbound replication link from an instance in another site.");
			Console.WriteLine("\t-Writable instance of the partition in the site has no outbound replication links.");
			Console.WriteLine("\t-Partition exists in only one site in the forest.  This does not affect the health of replication.");
		}

		private static bool saveData(string fileName)
		{
			XmlSerializer xs;
			try
			{
				xs = new XmlSerializer(typeof(List<DomainControllerReplicationInfo>));
			}
			catch (System.IO.IOException e)
			{
				Console.WriteLine("Error saving topology:\n\t{0}", e.Message);
				return false;
			}
			XmlTextWriter xtw = new XmlTextWriter(fileName, Encoding.Unicode);
			try
			{
				xs.Serialize(xtw, _dcs);
			}
			catch (InvalidOperationException e)
			{
				Console.WriteLine("Error saving topology:\n\t{0}", e.InnerException.Message);
				xtw.Close();
				return false;
			}
			catch (System.IO.IOException e)
			{
				Console.WriteLine("Error saving topology:\n\t{0}", e.Message);
				xtw.Close();
				return false;
			}
			//finally
			//{
			//    xtw.Close();
			//}
			xtw.Close();
			return true;
		}

		private static void outputHeader()
		{
			Console.WriteLine("Replication topology analyzer.  From http://www.codeplex.com/ActiveDirectoryUtils.");
			Console.WriteLine("Version:  {0}", System.Reflection.Assembly.GetExecutingAssembly().GetName().Version);
		}

		private static void CurrentDomain_UnhandledException(object sender, UnhandledExceptionEventArgs e)
		{
			Console.Clear();
			Console.WriteLine("This has experienced a critical error.  Please submit the below information to:\n http://www.codeplex.com/ActiveDirectoryUtils.");
			Console.WriteLine();
			outputHeader();
			Console.WriteLine(".NET Version: {0}", Environment.Version.ToString());
			Exception err = (Exception)e.ExceptionObject;
			Console.WriteLine("Type of Exception:\t{0}", err.GetType());
			Console.WriteLine("Error Message:\t{0}", err.Message);
			//Console.WriteLine("Stack Trace:\n{0}", err.StackTrace);
		}
	}
}
